package com.easyaop.api.support;

import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.implementation.InvokeDynamic;
import net.bytebuddy.implementation.bytecode.StackManipulation;
import net.bytebuddy.implementation.bytecode.assign.Assigner;
import net.bytebuddy.implementation.bytecode.member.MethodInvocation;
import net.bytebuddy.jar.asm.MethodVisitor;
import net.bytebuddy.utility.JavaConstant;

import java.lang.reflect.Field;
import java.util.List;

/**
 * @author lipan
 * @since 2025-02-19
 */
public class InvokeDynamicProxy extends InvokeDynamic {
    private static final Field F_BOOTSTRAP;

    private static final Field F_ARGUMENTS;

    private static final Field F_INVOCATION_PROVIDER;

    private static final Field F_TERMINATION_HANDLER;

    private static final Field F_ASSIGNER;

    private static final Field F_TYPING;

    protected InvokeDynamicProxy(MethodDescription.InDefinedShape bootstrap, List<? extends JavaConstant> arguments,
                                 InvocationProvider invocationProvider, TerminationHandler terminationHandler,
                                 Assigner assigner, Assigner.Typing typing) {
        super(bootstrap, arguments, invocationProvider, terminationHandler, assigner, typing);
    }

    public static InvokeDynamicProxy wrap(InvokeDynamic invokeDynamic) {
        MethodDescription.InDefinedShape bootstrap = getFieldValue(invokeDynamic, F_BOOTSTRAP,
                MethodDescription.InDefinedShape.class);
        List<? extends JavaConstant> arguments = getFieldValue(invokeDynamic, F_ARGUMENTS, List.class);
        InvocationProvider invocationProvider = getFieldValue(invokeDynamic, F_INVOCATION_PROVIDER,
                InvocationProvider.class);
        TerminationHandler terminationHandler = getFieldValue(invokeDynamic, F_TERMINATION_HANDLER,
                TerminationHandler.class);
        Assigner assigner = getFieldValue(invokeDynamic, F_ASSIGNER, Assigner.class);
        Assigner.Typing typing = getFieldValue(invokeDynamic, F_TYPING, Assigner.Typing.class);
        return new InvokeDynamicProxy(bootstrap, arguments, invocationProvider, terminationHandler, assigner, typing);
    }

    private static final <T> T getFieldValue(InvokeDynamic invokeDynamic, Field field, Class<T> clazz) {
        try {
            field.setAccessible(true);
            return (T) field.get(invokeDynamic);
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
    }

    public StackManipulation getStackManipulation(MethodDescription instrumentedMethod,
                                                  Assigner assigner,
                                                  Assigner.Typing typing) {
        InvocationProvider.Target.Resolved target =
                invocationProvider.make(instrumentedMethod)
                        .resolve(instrumentedMethod.getDeclaringType().asErasure(), assigner, typing);
        return new DefaultStackManipulation(instrumentedMethod.getDeclaringType().asErasure(), instrumentedMethod,
                bootstrap, arguments, target);
    }

    public static class DefaultStackManipulation extends StackManipulation.AbstractBase {
        private TypeDescription instrumentedType;

        private MethodDescription instrumentedMethod;

        private InvocationProvider.Target.Resolved target;

        private MethodDescription.InDefinedShape bootstrap;

        private List<? extends JavaConstant> arguments;

        public DefaultStackManipulation(TypeDescription instrumentedType,
                                        MethodDescription instrumentedMethod,
                                        MethodDescription.InDefinedShape bootstrap,
                                        List<? extends JavaConstant> arguments,
                                        InvocationProvider.Target.Resolved target) {
            this.instrumentedType = instrumentedType;
            this.instrumentedMethod = instrumentedMethod;
            this.target = target;
            this.bootstrap = bootstrap;
            this.arguments = arguments;
        }

        @Override
        public Size apply(MethodVisitor methodVisitor, Context implementationContext) {
            return new Compound(
                    target.getStackManipulation(),
                    MethodInvocation.invoke(bootstrap).dynamic(target.getInternalName(),
                            target.getReturnType(),
                            target.getParameterTypes(),
                            arguments)
            ).apply(methodVisitor, implementationContext);
        }
    }

    static {
        Class<InvokeDynamic> invokeDynamicClass = InvokeDynamic.class;
        Field f_bootstrap = null;
        Field f_arguments = null;
        Field f_invocation_provider = null;
        Field f_termination_handler = null;
        Field f_assigner = null;
        Field f_typing = null;
        try {
            f_bootstrap = invokeDynamicClass.getDeclaredField("bootstrap");
            f_arguments = invokeDynamicClass.getDeclaredField("arguments");
            f_invocation_provider = invokeDynamicClass.getDeclaredField("invocationProvider");
            f_termination_handler = invokeDynamicClass.getDeclaredField("terminationHandler");
            f_assigner = invokeDynamicClass.getDeclaredField("assigner");
            f_typing = invokeDynamicClass.getDeclaredField("typing");
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
        F_BOOTSTRAP = f_bootstrap;
        F_ARGUMENTS = f_arguments;
        F_INVOCATION_PROVIDER = f_invocation_provider;
        F_TERMINATION_HANDLER = f_termination_handler;
        F_ASSIGNER = f_assigner;
        F_TYPING = f_typing;
    }
}
